{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d95e2c6b",
   "metadata": {},
   "source": [
    "# Rebalancing Positions\n",
    "\n",
    "**PyBroker** can be used to simulate rebalancing a portfolio. This means that **PyBroker** can simulate adjusting the asset allocation of a portfolio to match a desired target allocation. Additionally, our portfolio can be rebalanced using [portfolio optimization](https://en.wikipedia.org/wiki/Portfolio_optimization), as this notebook will demonstrate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "1b3d19c9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<diskcache.core.Cache at 0x7efcf37e94c0>"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pybroker as pyb\n",
    "from datetime import datetime\n",
    "from pybroker import ExecContext, Strategy, YFinance\n",
    "\n",
    "pyb.enable_data_source_cache('rebalancing')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "807d42ab",
   "metadata": {},
   "source": [
    "## Equal Position Sizing\n",
    "\n",
    "Let's assume that we want to rebalance a long-only portfolio at the beginning of every month to make sure that each stock in our portfolio has a roughly equal allocation.\n",
    "\n",
    "We first start by writing a helper function to detect when the current bar's date is the start of a new month:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e4e129f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "def start_of_month(ctxs: dict[str, ExecContext]) -> bool:\n",
    "    dt = tuple(ctxs.values())[0].dt\n",
    "    if dt.month != pyb.param('current_month'):\n",
    "        pyb.param('current_month', dt.month)\n",
    "        return True\n",
    "    return False"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37b1ce80",
   "metadata": {},
   "source": [
    "Next, we implement a function that will either buy or sell enough shares of a stock to reach a target allocation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "4dcac652",
   "metadata": {},
   "outputs": [],
   "source": [
    "def set_target_shares(\n",
    "    ctxs: dict[str, ExecContext], \n",
    "    targets: dict[str, float]\n",
    "):\n",
    "    for symbol, target in targets.items():\n",
    "        ctx = ctxs[symbol]\n",
    "        target_shares = ctx.calc_target_shares(target)\n",
    "        pos = ctx.long_pos()\n",
    "        if pos is None:\n",
    "            ctx.buy_shares = target_shares\n",
    "        elif pos.shares < target_shares:\n",
    "            ctx.buy_shares = target_shares - pos.shares\n",
    "        elif pos.shares > target_shares:\n",
    "            ctx.sell_shares = pos.shares - target_shares"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef4d6cfd",
   "metadata": {},
   "source": [
    "If the current allocation is above the target level, the function will sell the needed shares of the asset, while if the current allocation is below the target level, the function will buy the needed shares of the asset.\n",
    "\n",
    "Following that, we write a ``rebalance`` function to target each asset to an equal allocation at the beginning of every month:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "2ab0fbfe",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rebalance(ctxs: dict[str, ExecContext]):\n",
    "    if start_of_month(ctxs):\n",
    "        target = 1 / len(ctxs)\n",
    "        set_target_shares(ctxs, {symbol: target for symbol in ctxs.keys()})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb714864",
   "metadata": {},
   "source": [
    "Now that we have implemented the ``rebalance`` function, the next step is to backtest our rebalancing strategy using a portfolio of five stocks. To process all stocks at once on each bar of data, we will use the [Strategy#set_after_exec](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy.set_after_exec) method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "4e74fa41",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00\n",
      "\n",
      "Loading bar data...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[*********************100%***********************]  5 of 5 completed"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Loaded bar data: 0:00:01 \n",
      "\n",
      "Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(1259 of 1259)\u001b[39m |####################| Elapsed Time: 0:00:01 Time:  0:00:010000\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Finished backtest: 0:00:04\n"
     ]
    }
   ],
   "source": [
    "strategy = Strategy(YFinance(), start_date='1/1/2018', end_date='1/1/2023')\n",
    "strategy.add_execution(None, ['TSLA', 'NFLX', 'AAPL', 'NVDA', 'AMZN'])\n",
    "strategy.set_after_exec(rebalance)\n",
    "result = strategy.backtest()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2db2b1db",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>type</th>\n",
       "      <th>symbol</th>\n",
       "      <th>date</th>\n",
       "      <th>shares</th>\n",
       "      <th>limit_price</th>\n",
       "      <th>fill_price</th>\n",
       "      <th>fees</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>id</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>buy</td>\n",
       "      <td>AAPL</td>\n",
       "      <td>2018-01-03</td>\n",
       "      <td>464</td>\n",
       "      <td>NaN</td>\n",
       "      <td>43.31</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2018-01-03</td>\n",
       "      <td>336</td>\n",
       "      <td>NaN</td>\n",
       "      <td>59.84</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>buy</td>\n",
       "      <td>NFLX</td>\n",
       "      <td>2018-01-03</td>\n",
       "      <td>99</td>\n",
       "      <td>NaN</td>\n",
       "      <td>203.86</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>buy</td>\n",
       "      <td>NVDA</td>\n",
       "      <td>2018-01-03</td>\n",
       "      <td>4013</td>\n",
       "      <td>NaN</td>\n",
       "      <td>5.22</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>buy</td>\n",
       "      <td>TSLA</td>\n",
       "      <td>2018-01-03</td>\n",
       "      <td>873</td>\n",
       "      <td>NaN</td>\n",
       "      <td>21.36</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>293</th>\n",
       "      <td>sell</td>\n",
       "      <td>NFLX</td>\n",
       "      <td>2022-12-02</td>\n",
       "      <td>15</td>\n",
       "      <td>NaN</td>\n",
       "      <td>315.99</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>294</th>\n",
       "      <td>sell</td>\n",
       "      <td>NVDA</td>\n",
       "      <td>2022-12-02</td>\n",
       "      <td>974</td>\n",
       "      <td>NaN</td>\n",
       "      <td>16.69</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>295</th>\n",
       "      <td>buy</td>\n",
       "      <td>AAPL</td>\n",
       "      <td>2022-12-02</td>\n",
       "      <td>28</td>\n",
       "      <td>NaN</td>\n",
       "      <td>146.82</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>296</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2022-12-02</td>\n",
       "      <td>42</td>\n",
       "      <td>NaN</td>\n",
       "      <td>94.57</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>297</th>\n",
       "      <td>buy</td>\n",
       "      <td>TSLA</td>\n",
       "      <td>2022-12-02</td>\n",
       "      <td>70</td>\n",
       "      <td>NaN</td>\n",
       "      <td>193.68</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>297 rows × 7 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "     type symbol       date  shares  limit_price  fill_price  fees\n",
       "id                                                                \n",
       "1     buy   AAPL 2018-01-03     464          NaN       43.31   0.0\n",
       "2     buy   AMZN 2018-01-03     336          NaN       59.84   0.0\n",
       "3     buy   NFLX 2018-01-03      99          NaN      203.86   0.0\n",
       "4     buy   NVDA 2018-01-03    4013          NaN        5.22   0.0\n",
       "5     buy   TSLA 2018-01-03     873          NaN       21.36   0.0\n",
       "..    ...    ...        ...     ...          ...         ...   ...\n",
       "293  sell   NFLX 2022-12-02      15          NaN      315.99   0.0\n",
       "294  sell   NVDA 2022-12-02     974          NaN       16.69   0.0\n",
       "295   buy   AAPL 2022-12-02      28          NaN      146.82   0.0\n",
       "296   buy   AMZN 2022-12-02      42          NaN       94.57   0.0\n",
       "297   buy   TSLA 2022-12-02      70          NaN      193.68   0.0\n",
       "\n",
       "[297 rows x 7 columns]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result.orders"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81452ad7",
   "metadata": {},
   "source": [
    "## Portfolio Optimization\n",
    "\n",
    "[Portfolio optimization](https://en.wikipedia.org/wiki/Portfolio_optimization) can guide our rebalancing in order to meet some objective for our portfolio. For instance, we can use portfolio optimization with the goal of allocating stocks in a way to minimize risk.\n",
    "\n",
    "[Riskfolio-Lib](https://riskfolio-lib.readthedocs.io/) is a popular Python library for performing portfolio optimization. Below shows how to use it to construct a minimum risk portfolio by minimizing the portfolio's [Conditional Value at Risk (CVar)](https://www.investopedia.com/terms/c/conditional_value_at_risk.asp) based on the past year of returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "128204be",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import riskfolio as rp\n",
    "\n",
    "pyb.param('lookback', 252)  # Use past year of returns.\n",
    "\n",
    "def calculate_returns(ctxs: dict[str, ExecContext], lookback: int):\n",
    "    prices = {}\n",
    "    for ctx in ctxs.values():\n",
    "        prices[ctx.symbol] = ctx.adj_close[-lookback:]\n",
    "    df = pd.DataFrame(prices)\n",
    "    return df.pct_change().dropna()\n",
    "\n",
    "def optimization(ctxs: dict[str, ExecContext]):\n",
    "    lookback = pyb.param('lookback')\n",
    "    if start_of_month(ctxs):\n",
    "        Y = calculate_returns(ctxs, lookback)\n",
    "        port = rp.Portfolio(returns=Y)\n",
    "        port.assets_stats(method_mu='hist', method_cov='hist')\n",
    "        w = port.optimization(\n",
    "            model='Classic', \n",
    "            rm='CVaR', \n",
    "            obj='MinRisk', \n",
    "            rf=0,      # Risk free rate.\n",
    "            l=0,       # Risk aversion factor.\n",
    "            hist=True  # Use historical scenarios.\n",
    "        )\n",
    "        targets = {\n",
    "            symbol: w.T[symbol].values[0]\n",
    "            for symbol in ctxs.keys()\n",
    "        }\n",
    "        set_target_shares(ctxs, targets)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bec587f2",
   "metadata": {},
   "source": [
    "You can find more information and examples of using [Riskfolio-Lib](https://riskfolio-lib.readthedocs.io/) on its official documentation. Now, let's move on to backtesting the strategy!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "7353fc62",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00\n",
      "\n",
      "Loaded cached bar data.\n",
      "\n",
      "Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(1259 of 1259)\u001b[39m |####################| Elapsed Time: 0:00:01 Time:  0:00:010000\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Finished backtest: 0:00:01\n"
     ]
    }
   ],
   "source": [
    "strategy.set_after_exec(optimization)\n",
    "result = strategy.backtest(warmup=pyb.param('lookback'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "95b1e1d8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>type</th>\n",
       "      <th>symbol</th>\n",
       "      <th>date</th>\n",
       "      <th>shares</th>\n",
       "      <th>limit_price</th>\n",
       "      <th>fill_price</th>\n",
       "      <th>fees</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>id</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>buy</td>\n",
       "      <td>AAPL</td>\n",
       "      <td>2019-01-04</td>\n",
       "      <td>1420</td>\n",
       "      <td>NaN</td>\n",
       "      <td>36.54</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2019-01-04</td>\n",
       "      <td>347</td>\n",
       "      <td>NaN</td>\n",
       "      <td>77.81</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>buy</td>\n",
       "      <td>TSLA</td>\n",
       "      <td>2019-01-04</td>\n",
       "      <td>1020</td>\n",
       "      <td>NaN</td>\n",
       "      <td>20.69</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>sell</td>\n",
       "      <td>AAPL</td>\n",
       "      <td>2019-02-04</td>\n",
       "      <td>103</td>\n",
       "      <td>NaN</td>\n",
       "      <td>42.37</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2019-02-04</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>81.58</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    type symbol       date  shares  limit_price  fill_price  fees\n",
       "id                                                               \n",
       "1    buy   AAPL 2019-01-04    1420          NaN       36.54   0.0\n",
       "2    buy   AMZN 2019-01-04     347          NaN       77.81   0.0\n",
       "3    buy   TSLA 2019-01-04    1020          NaN       20.69   0.0\n",
       "4   sell   AAPL 2019-02-04     103          NaN       42.37   0.0\n",
       "5    buy   AMZN 2019-02-04       1          NaN       81.58   0.0"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result.orders.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf076936",
   "metadata": {},
   "source": [
    "Above, we can observe that the portfolio optimization resulted in allocating the entire portfolio to 3 of the 5 stocks during the first month of the backtest."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
